package org.vaadin.vaadinjit.data;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Default implementation for data structure used by chart visualizations.
 * 
 * @author jonkka
 *
 */
public class DefaultChartData implements JitChartData {

	private List<String> categories;
	private List<String> dataPoints;
	private List<Value> values;
	
	private boolean notifyImmediately;
	
	private List<JitChartData.ChangeListener> changeListeners;

	/**
	 * Create a new instance
	 */
	public DefaultChartData() {
		categories = new ArrayList<String>();
		dataPoints = new ArrayList<String>();
		values = new ArrayList<Value>();
		changeListeners = new ArrayList<JitChartData.ChangeListener>();
		notifyImmediately = true;

	}
	
	
	@Override
	public void addCategory(String label) {
		categories.add(label);
		notifyLabelAdded(label);
	}

	@Override
	public void addDataPoint(String dataPoint) {
		dataPoints.add(dataPoint);
		notifyDataPointAdded(dataPoint);
	}

	@Override
	public void setValue(String label, String dataPoint, double value) {
		if (!categoryExists(label))
			throw new RuntimeException(
					"Category with name " + label + " does not exist");
		if (!dataPointExists(dataPoint))
			throw new RuntimeException(
					"Data point with name " + dataPoint + " does not exist");
		
		Value val = getVal(label, dataPoint);
				
		if (val == null) {
			val = new Value(label, dataPoint, value);
			values.add(val);
		}
		else {
			val.setValue(value);
		}
		if (notifyImmediately)
			notifyValueSet(label, dataPoint, value);
	}
	
	@Override
	public void setValue(String label, String dataPoint, int value) {
		setValue(label, dataPoint, (float)value);
	}


	@Override
	public double getValue(String category, String dataPoint) {
		if (!categoryExists(category))
			throw new RuntimeException(
					"Category with name " + category + " does not exist");
		if (!dataPointExists(dataPoint))
			throw new RuntimeException(
					"Data point with name " + dataPoint + " does not exist");
		
		Value v = getVal(category, dataPoint);
		
		if (v != null)
			return v.getValue();
		
		return 0;
	}

	
	@Override
	public void addChangeListener(ChangeListener listener) {
		changeListeners.add(listener);
	}

	@Override
	public List<String> getCategories() {
		return categories;
	}

	@Override
	public List<String> getDataPoints() {
		return dataPoints;
	}
	
	@Override
	public String toJSON() {
		StringBuilder sBuilder = new StringBuilder();
		sBuilder.append("{");
		sBuilder.append("'label': ");
		sBuilder.append("[");
		for (Iterator<String> iter = categories.iterator(); iter.hasNext();) {
			
			String label = iter.next();
			sBuilder.append("'" + label + "'");
			if (iter.hasNext())
				sBuilder.append(",");
		}
		sBuilder.append("], ");
		sBuilder.append("'values': ");
		sBuilder.append("[");
		for (Iterator<String> iter = dataPoints.iterator(); iter.hasNext();) {

			String dataPoint = iter.next();
			sBuilder.append("{");
			sBuilder.append("'label': '" + dataPoint + "',");

			sBuilder.append("'values': ["  );
			for (Iterator<String> iter2 = categories.iterator(); iter2.hasNext();) {

				String category = iter2.next();
				Value v = getVal(category, dataPoint);

				double value;
				if (v == null)
					value = 0;
				else
					value = v.getValue();
				
				sBuilder.append(value);
				if (iter2.hasNext()) {
					sBuilder.append(",");
				}
				
			}
			sBuilder.append("]");
			sBuilder.append("}");
			if (iter.hasNext())
				sBuilder.append(",");
		}
		sBuilder.append("]");
		
		sBuilder.append("}");
		return sBuilder.toString();
	}

	/**
	 * Notify listeners that the data has changed
	 */
	public void notifyChanged() {
		for (JitChartData.ChangeListener l : changeListeners)
			l.valuesChanged();
	}
	
	
	/**
	 * If set to true the visualizations will notify listeners about changes
	 * immediately when changed.
	 * 
	 * @param value
	 */
	public void setNotifyListenersImmediately(boolean value) {
		notifyImmediately = value;
	}
	
	/**
	 * Tell if this data structure notifies its listeners immediately
	 * 
	 * @return true if this visualization notifies immediately
	 */
	public boolean getNotifyListenersImmediately() {
		return notifyImmediately;
	}

	private boolean dataPointExists(String dataPoint) {
		for (String d : dataPoints) {
			if (d.equals(dataPoint))
				return true;
		}
		return false;
	}

	private boolean categoryExists(String label) {
		for (String l : categories) {
			if (l.equals(label))
				return true;
		}
		return false;
	}
	

	private Value getVal(String category, String dataPoint) {
		for (Value v : values) {
			if (v.getDataPoint().equalsIgnoreCase(dataPoint) &&
					v.getCategory().equalsIgnoreCase(category)) {

				return v;
			}
		}
		return null;
	}
	
	private void notifyValueSet(String label, String dataPoint, double value) {
		for (ChangeListener l : changeListeners) {
			l.valueSet(label, dataPoint, value);
		}
	}
	
	
	private void notifyDataPointAdded(String dataPoint) {
		for (ChangeListener l : changeListeners) {
			l.dataPointAdded(dataPoint);
		}
	}
	
	private void notifyLabelAdded(String label) {
		for (ChangeListener l : changeListeners) {
			l.categoryAdded(label);
		}
	}
	
	/**
	 * Internal class used to store values
	 * 
	 * @author jonkka
	 *
	 */
	class Value {
		
		private String dataPoint;
		private String category;
		private double value;
		private boolean changed;
		
		/**
		 * Create new value
		 * 
		 * @param category
		 * @param dataPoint
		 * @param value
		 */
		Value(String category, String dataPoint, double value) {
			this.dataPoint = dataPoint;
			this.category = category;
			this.value = value;
		}

		/**
		 * Get the data point of this value
		 * 
		 * @return the data point
		 */
		public String getDataPoint() {
			return dataPoint;
		}

		/**
		 * Get the category of this value
		 * 
		 * @return the category
		 */
		public String getCategory() {
			return category;
		}

		/**
		 * Set the actual value of this (category, dataPoint) pair
		 * 
		 * @param value the value
		 */
		public void setValue(double value) {
			this.value = value;
		}

		
		/**
		 * Get the value of this pair
		 * 
		 * @return the value
		 */
		public double getValue() {
			return value;
		}
		
		/**
		 * Tells if this value has changed
		 * 
		 * @return
		 */
		public boolean hasChanged() {
			return changed;
		}
		
		/**
		 * Mark this value as changed or not changed
		 * 
		 * @param value true if changed, false otherwise
		 * @return
		 */
		public void setChanged(boolean value) {
			changed = value;
		}
		
		
		@Override
		public boolean equals(Object obj) {
			if (obj instanceof Value) {
				Value v = (Value) obj;
				
				if (this.getDataPoint().equals(v.getDataPoint()) &&
						this.getCategory().equals(v.getCategory())) {
					
					return true;
				}
			}
			return false;
		}
		
	}
}
